feat(local): add local dev server for capturing SDK events#888
Conversation
Adds 'sentry local', a long-running command that starts a minimal Hono
HTTP server wire-compatible with the Spotlight sidecar protocol. The
server uses @spotlightjs/spotlight/sdk's createSpotlightBuffer +
pushToSpotlightBuffer helpers to ingest envelopes from any Sentry SDK
running in the user's dev stack and tails them to the terminal.
Endpoints exposed:
POST /stream - Spotlight ingest
POST /api/{projectId}/envelope/ - Sentry SDK ingest path
GET /stream - SSE feed for the Spotlight overlay
GET /health - liveness check
Why a thin in-tree server instead of spawning npx @spotlightjs/spotlight:
the SDK helpers give us decompression + lazy parsing for free while
keeping the surface focused on a CLI-friendly tail UX, and bundling
through esbuild keeps the published binary self-contained per the
no-runtime-dependencies rule.
The command runs without auth (it's a local dev tool) and shuts down
gracefully on SIGINT/SIGTERM, force-closing keep-alive connections so
SSE subscribers don't block exit.
|
Codecov Results 📊✅ 7137 passed | Total: 7137 | Pass Rate: 100% | Execution Time: 0ms 📊 Comparison with Base Branch
All tests are passing successfully. ❌ Patch coverage is 50.75%. Project has 15233 uncovered lines. Files with missing lines (3)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 76.62% 76.23% -0.39%
==========================================
Files 324 328 +4
Lines 63171 64072 +901
Branches 0 0 —
==========================================
+ Hits 48396 48839 +443
- Misses 14775 15233 +458
- Partials 0 0 —Generated by Codecov Action |
Replace the minimal 'timestamp • type' one-liner with rich formatted output that shows actual event content: error type/message with stack location, transaction name/op/duration/span count, and log messages with attributes. Uses the CLI's own color system since Spotlight's humanFormatters aren't publicly exported from the package.
Adds a repeatable --filter flag that accepts error, transaction, or log. When set, only matching envelope items are rendered in the tail output; non-matching items are silently dropped. No filter = show everything. Usage: sentry local -f error # errors only sentry local -f error -f log # errors and logs sentry local -f transaction # transactions only
Three fixes based on audit against Spotlight's reference implementation: 1. Signal handling: process.once -> process.on so the 'second signal = force exit' code path is reachable (process.once unregisters after the first signal, making the shuttingDown check dead code). 2. SSE format: match the Spotlight protocol so the overlay UI works. - event name is the content type (not 'envelope') - id field is the Spotlight-assigned envelope UUID - data is the parsed envelope JSON (not base64-encoded raw bytes) - Last-Event-ID reconnection is now supported 3. Browser SDK: detect sendBeacon() payloads (Content-Type: text/plain with sentry_client query param) and override to the canonical application/x-sentry-envelope, matching Spotlight's workaround.
The --open flag opened the raw SSE endpoint in a browser, which just shows streaming text — not useful without the Spotlight overlay UI. Removed it and updated the fragment docs to document the new pretty-print tail output and --filter flag instead.
- Remove logger tag so 'local <timestamp>' no longer clutters every line - Remove 'Spotlight sidecar' wording, use 'Listening on <url>' instead - Remove endpoint listing and DSN instructions from banner - Add Spotlight docs link for getting started - Auto-increment port on EADDRINUSE (up to 10 attempts)
- Remove pointless `log = logger` alias; use `logger` directly - Strip narrating comments that restate the code - Rename buildSidecarApp → buildApp; drop remaining 'sidecar' references - Make onEnvelope callback optional instead of passing noop - Remove redundant type annotation on activeFilters
Startup banner keeps logger.info (shows ℹ icon), while tail output and shutdown messages use logger.log (no icon prefix).
The wildcard origin allowed any webpage to connect to the SSE stream and exfiltrate envelope data. Restrict to localhost/127.0.0.1 origins which is sufficient for local dev stacks (Vite, Next, Astro, etc.).
- Fix subscription leak: merge dual stream.onAbort() into one callback so unsubscribe and promise resolution both fire on disconnect - Sanitize envelope content with stripAnsi() before rendering to terminal to prevent ANSI escape injection from crafted payloads - Add 10 MB body size guard on ingest to reject oversized payloads (returns 413)
- Type labels: uppercase, bracketed, padded — [ERROR] [TRACE] [INFO] - Source labels: uppercase, bracketed, padded — [SERVER] [BROWSER] [MOBILE] - Source colors: match Spotlight Sentinel theme (mobile=blue) - Log attributes: per-attribute brackets [key=value] [key=value] - Update docs fragment example output to match
exception.values is ordered oldest→newest per the Sentry protocol, so values[0] is the root cause. Use .at(-1) to display the outermost exception, matching Sentry UI and Spotlight behavior.
|
fix-ci: attempt 1 — flaky property test |
@sentry/sqlish uppercases SQL keywords (e.g. "by" → "BY"), so identifiers that happen to match keywords fail the strict equality check. Compare lowercased strings instead.
- On startup, probe the target port for an existing Spotlight server. If one is running, attach as an SSE consumer instead of starting a duplicate server. Uses fetch-based SSE parsing since Bun lacks global EventSource. - Last-Event-ID reconnection already supported via the Spotlight SDK's subscribe(callback, lastEventId) parameter. - Port retry now uses 3 retries with 5s backoff (matching Spotlight) instead of 10 sequential port increments.
…rigins Returning a mismatched string still worked (browser blocks on mismatch) but returning null correctly omits the Access-Control-Allow-Origin header per Hono's CORS middleware API.
|
fix-ci: attempt 1 — biome formatting issue in |
… control chars - run: set process.exitCode directly instead of EXIT.GENERAL so callers can distinguish the child's error type. - server: validate content-encoding header against known values before passing to pushToSpotlightBuffer. - sanitize: strip C0 control characters (BEL, BS, etc.) in addition to ANSI escapes and newlines.
- waitForShutdown: remove SIGINT/SIGTERM listeners after the first signal fires, preventing handler accumulation. - consumer mode: wrap consumeSSE in try/finally to remove signal handlers on both normal exit and error paths.
tryListen now reads the OS-assigned port from server.address() instead of returning the original argument. run.ts builds spotlightUrl after the server starts so the child process gets the correct URL.
…el colors - run: revert to throwing CliError with the child's actual exit code instead of setting process.exitCode, matching test expectations. - formatType: use lowercase level for LEVEL_COLORS lookup so uppercase levels like 'INFO' still get colored output.
… separator - Use ...args rest parameter (Stricli passes variadic positionals as individual params, not an array) - Strip leading '--' separator that Stricli passes through
Without signal forwarding, SIGTERM sent to the parent doesn't reach the grandchild process (e.g. bun run app.ts), causing the run command to hang on shutdown.
|
fix-ci budget exhausted (3 prior attempts), but this failure was already addressed in ff3c8b8 (pushed before this CI notification arrived). The test was passing args as an array instead of rest params — fixed by spreading them individually. CI is running on the fix now. |
…n type The buildCommand wrapper consumes the async generator internally, so loader() returns a plain async function. Tests must spread args as individual parameters (matching Stricli's variadic positional convention) and await the Promise directly.
The reverse order can prevent the close callback from firing when active SSE connections exist, causing the shutdown promise to hang.
- CORS: add [::1] to LOCALHOST_ORIGIN_RE so IPv6 dev stacks pass preflight. - formatSingleLog: guard against null attribute entries before accessing v.value. - run.ts: remove unused biome-ignore suppression.
Extend sanitize() to cover the C1 range (0x80-0x9F) which includes raw 8-bit CSI/OSC/DCS introducers, and collapse NEL (U+0085) as a line break.
|
the Secret Scan failure is a false positive — it's flagging the pnpm patch hash for |
…ze() Add a pass to remove bidi marks (U+200E-200F, U+202A-202E, U+2066-2069) that can reorder terminal output rendering.
Summary
Adds
sentry local, a local development server that captures Sentry SDK envelopes and tails errors, traces, and logs to the terminal in real time.Two subcommands:
sentry local serve(default) — starts the server and tails events. If a server is already running on the port, attaches as an SSE consumer instead.sentry local run -- <cmd>— runs a command withSENTRY_SPOTLIGHTinjected so the SDK auto-sends envelopes to the local server. No code changes needed.What's new
src/commands/local/serve.ts— the server command (default). Flags:--port/-p,--host/-H,--quiet/-q,--filter/-f.src/commands/local/run.ts— child process wrapper. InjectsSENTRY_SPOTLIGHT,NEXT_PUBLIC_SENTRY_SPOTLIGHT,SENTRY_TRACES_SAMPLE_RATE=1.src/commands/local/index.ts— route map withserve(default) andrun.package.json— addshono,@hono/node-server,@spotlightjs/spotlightas devDependencies.docs/src/fragments/commands/local.md— examples, env var table, endpoint reference.Key behaviors
/healthon startup — attaches as SSE consumer if a server is already running.Endpoints (serve mode)
POST/streamPOST/api/{projectId}/envelope/GET/streamGET/health